/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package qunar.tc.bistoury.instrument.client.spring.el;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * Manages the class being generated by the compilation process.
 *
 * <p>Records intermediate compilation state as the bytecode is generated.
 * Also includes various bytecode generation helper functions.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @since 4.1
 */
class CodeFlow implements Opcodes {

    /**
     * Name of the class being generated. Typically used when generating code
     * that accesses freshly generated fields on the generated type.
     */
    private final String className;

    /**
     * The current class being generated.
     */
    private final ClassWriter classWriter;

    /**
     * Record the type of what is on top of the bytecode stack (i.e. the type of the
     * output from the previous expression component). New scopes are used to evaluate
     * sub-expressions like the expressions for the argument values in a method invocation
     * expression.
     */
    private final Stack<ArrayList<String>> compilationScopes;

    /**
     * As SpEL ast nodes are called to generate code for the main evaluation method
     * they can register to add a field to this class. Any registered FieldAdders
     * will be called after the main evaluation function has finished being generated.
     */
    private List<FieldAdder> fieldAdders;

    /**
     * As SpEL ast nodes are called to generate code for the main evaluation method
     * they can register to add code to a static initializer in the class. Any
     * registered ClinitAdders will be called after the main evaluation function
     * has finished being generated.
     */
    private List<ClinitAdder> clinitAdders;

    /**
     * When code generation requires holding a value in a class level field, this
     * is used to track the next available field id (used as a name suffix).
     */
    private int nextFieldId = 1;

    /**
     * When code generation requires an intermediate variable within a method,
     * this method records the next available variable (variable 0 is 'this').
     */
    private int nextFreeVariableId = 1;


    /**
     * Construct a new {@code CodeFlow} for the given class.
     *
     * @param className   the name of the class
     * @param classWriter the corresponding ASM {@code ClassWriter}
     */
    public CodeFlow(String className, ClassWriter classWriter) {
        this.className = className;
        this.classWriter = classWriter;
        this.compilationScopes = new Stack<ArrayList<String>>();
        this.compilationScopes.add(new ArrayList<String>());
    }


    /**
     * Push the byte code to load the target (i.e. what was passed as the first argument
     * to CompiledExpression.getValue(target, context))
     *
     * @param mv the visitor into which the load instruction should be inserted
     */
    public void loadTarget(MethodVisitor mv) {
        mv.visitVarInsn(ALOAD, 1);
    }

    /**
     * Push the bytecode to load the EvaluationContext (the second parameter passed to
     * the compiled expression method).
     *
     * @param mv the visitor into which the load instruction should be inserted
     * @since 4.3.4
     */
    public void loadEvaluationContext(MethodVisitor mv) {
        mv.visitVarInsn(ALOAD, 2);
    }

    /**
     * Record the descriptor for the most recently evaluated expression element.
     *
     * @param descriptor type descriptor for most recently evaluated element
     */
    public void pushDescriptor(String descriptor) {
        Assert.notNull(descriptor, "Descriptor must not be null");
        this.compilationScopes.peek().add(descriptor);
    }

    /**
     * Enter a new compilation scope, usually due to nested expression evaluation. For
     * example when the arguments for a method invocation expression are being evaluated,
     * each argument will be evaluated in a new scope.
     */
    public void enterCompilationScope() {
        this.compilationScopes.push(new ArrayList<String>());
    }

    /**
     * Exit a compilation scope, usually after a nested expression has been evaluated. For
     * example after an argument for a method invocation has been evaluated this method
     * returns us to the previous (outer) scope.
     */
    public void exitCompilationScope() {
        this.compilationScopes.pop();
    }

    /**
     * Return the descriptor for the item currently on top of the stack (in the current scope).
     */
    public String lastDescriptor() {
        if (this.compilationScopes.peek().isEmpty()) {
            return null;
        }
        return this.compilationScopes.peek().get(this.compilationScopes.peek().size() - 1);
    }

    /**
     * If the codeflow shows the last expression evaluated to java.lang.Boolean then
     * insert the necessary instructions to unbox that to a boolean primitive.
     *
     * @param mv the visitor into which new instructions should be inserted
     */
    public void unboxBooleanIfNecessary(MethodVisitor mv) {
        if (lastDescriptor().equals("Ljava/lang/Boolean")) {
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
        }
    }

    /**
     * Called after the main expression evaluation method has been generated, this
     * method will callback any registered FieldAdders or ClinitAdders to add any
     * extra information to the class representing the compiled expression.
     */
    public void finish() {
        if (this.fieldAdders != null) {
            for (FieldAdder fieldAdder : this.fieldAdders) {
                fieldAdder.generateField(this.classWriter, this);
            }
        }
        if (this.clinitAdders != null) {
            MethodVisitor mv = this.classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
            mv.visitCode();
            this.nextFreeVariableId = 0;  // to 0 because there is no 'this' in a clinit
            for (ClinitAdder clinitAdder : this.clinitAdders) {
                clinitAdder.generateCode(mv, this);
            }
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);  // not supplied due to COMPUTE_MAXS
            mv.visitEnd();
        }
    }

    /**
     * Register a FieldAdder which will add a new field to the generated
     * class to support the code produced by an ast nodes primary
     * generateCode() method.
     */
    public void registerNewField(FieldAdder fieldAdder) {
        if (this.fieldAdders == null) {
            this.fieldAdders = new ArrayList<FieldAdder>();
        }
        this.fieldAdders.add(fieldAdder);
    }

    /**
     * Register a ClinitAdder which will add code to the static
     * initializer in the generated class to support the code
     * produced by an ast nodes primary generateCode() method.
     */
    public void registerNewClinit(ClinitAdder clinitAdder) {
        if (this.clinitAdders == null) {
            this.clinitAdders = new ArrayList<ClinitAdder>();
        }
        this.clinitAdders.add(clinitAdder);
    }

    public int nextFieldId() {
        return this.nextFieldId++;
    }

    public int nextFreeVariableId() {
        return this.nextFreeVariableId++;
    }

    public String getClassName() {
        return this.className;
    }

    @Deprecated
    public String getClassname() {
        return this.className;
    }


    /**
     * Insert any necessary cast and value call to convert from a boxed type to a
     * primitive value.
     *
     * @param mv              the method visitor into which instructions should be inserted
     * @param ch              the primitive type desired as output
     * @param stackDescriptor the descriptor of the type on top of the stack
     */
    public static void insertUnboxInsns(MethodVisitor mv, char ch, String stackDescriptor) {
        switch (ch) {
            case 'Z':
                if (!stackDescriptor.equals("Ljava/lang/Boolean")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
                break;
            case 'B':
                if (!stackDescriptor.equals("Ljava/lang/Byte")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
                break;
            case 'C':
                if (!stackDescriptor.equals("Ljava/lang/Character")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false);
                break;
            case 'D':
                if (!stackDescriptor.equals("Ljava/lang/Double")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false);
                break;
            case 'F':
                if (!stackDescriptor.equals("Ljava/lang/Float")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
                break;
            case 'I':
                if (!stackDescriptor.equals("Ljava/lang/Integer")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
                break;
            case 'J':
                if (!stackDescriptor.equals("Ljava/lang/Long")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
                break;
            case 'S':
                if (!stackDescriptor.equals("Ljava/lang/Short")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
                break;
            default:
                throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'");
        }
    }

    /**
     * For numbers, use the appropriate method on the number to convert it to the primitive type requested.
     *
     * @param mv               the method visitor into which instructions should be inserted
     * @param targetDescriptor the primitive type desired as output
     * @param stackDescriptor  the descriptor of the type on top of the stack
     */
    public static void insertUnboxNumberInsns(MethodVisitor mv, char targetDescriptor, String stackDescriptor) {
        switch (targetDescriptor) {
            case 'D':
                if (stackDescriptor.equals("Ljava/lang/Object")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "doubleValue", "()D", false);
                break;
            case 'F':
                if (stackDescriptor.equals("Ljava/lang/Object")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "floatValue", "()F", false);
                break;
            case 'J':
                if (stackDescriptor.equals("Ljava/lang/Object")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "longValue", "()J", false);
                break;
            case 'I':
                if (stackDescriptor.equals("Ljava/lang/Object")) {
                    mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
                }
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "intValue", "()I", false);
                break;
            // does not handle Z, B, C, S
            default:
                throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + targetDescriptor + "'");
        }
    }

    /**
     * Insert any necessary numeric conversion bytecodes based upon what is on the stack and the desired target type.
     *
     * @param mv               the method visitor into which instructions should be placed
     * @param targetDescriptor the (primitive) descriptor of the target type
     * @param stackDescriptor  the descriptor of the operand on top of the stack
     */
    public static void insertAnyNecessaryTypeConversionBytecodes(MethodVisitor mv, char targetDescriptor, String stackDescriptor) {
        if (CodeFlow.isPrimitive(stackDescriptor)) {
            char stackTop = stackDescriptor.charAt(0);
            if (stackTop == 'I' || stackTop == 'B' || stackTop == 'S' || stackTop == 'C') {
                if (targetDescriptor == 'D') {
                    mv.visitInsn(I2D);
                } else if (targetDescriptor == 'F') {
                    mv.visitInsn(I2F);
                } else if (targetDescriptor == 'J') {
                    mv.visitInsn(I2L);
                } else if (targetDescriptor == 'I') {
                    // nop
                } else {
                    throw new IllegalStateException("cannot get from " + stackTop + " to " + targetDescriptor);
                }
            } else if (stackTop == 'J') {
                if (targetDescriptor == 'D') {
                    mv.visitInsn(L2D);
                } else if (targetDescriptor == 'F') {
                    mv.visitInsn(L2F);
                } else if (targetDescriptor == 'J') {
                    // nop
                } else if (targetDescriptor == 'I') {
                    mv.visitInsn(L2I);
                } else {
                    throw new IllegalStateException("cannot get from " + stackTop + " to " + targetDescriptor);
                }
            } else if (stackTop == 'F') {
                if (targetDescriptor == 'D') {
                    mv.visitInsn(F2D);
                } else if (targetDescriptor == 'F') {
                    // nop
                } else if (targetDescriptor == 'J') {
                    mv.visitInsn(F2L);
                } else if (targetDescriptor == 'I') {
                    mv.visitInsn(F2I);
                } else {
                    throw new IllegalStateException("cannot get from " + stackTop + " to " + targetDescriptor);
                }
            } else if (stackTop == 'D') {
                if (targetDescriptor == 'D') {
                    // nop
                } else if (targetDescriptor == 'F') {
                    mv.visitInsn(D2F);
                } else if (targetDescriptor == 'J') {
                    mv.visitInsn(D2L);
                } else if (targetDescriptor == 'I') {
                    mv.visitInsn(D2I);
                } else {
                    throw new IllegalStateException("cannot get from " + stackDescriptor + " to " + targetDescriptor);
                }
            }
        }
    }


    /**
     * Create the JVM signature descriptor for a method. This consists of the descriptors
     * for the method parameters surrounded with parentheses, followed by the
     * descriptor for the return type. Note the descriptors here are JVM descriptors,
     * unlike the other descriptor forms the compiler is using which do not include the
     * trailing semicolon.
     *
     * @param method the method
     * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V")
     */
    public static String createSignatureDescriptor(Method method) {
        Class<?>[] params = method.getParameterTypes();
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        for (Class<?> param : params) {
            sb.append(toJvmDescriptor(param));
        }
        sb.append(")");
        sb.append(toJvmDescriptor(method.getReturnType()));
        return sb.toString();
    }

    /**
     * Create the JVM signature descriptor for a constructor. This consists of the
     * descriptors for the constructor parameters surrounded with parentheses, followed by
     * the descriptor for the return type, which is always "V". Note the
     * descriptors here are JVM descriptors, unlike the other descriptor forms the
     * compiler is using which do not include the trailing semicolon.
     *
     * @param ctor the constructor
     * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V")
     */
    public static String createSignatureDescriptor(Constructor<?> ctor) {
        Class<?>[] params = ctor.getParameterTypes();
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        for (Class<?> param : params) {
            sb.append(toJvmDescriptor(param));
        }
        sb.append(")V");
        return sb.toString();
    }

    /**
     * Determine the JVM descriptor for a specified class. Unlike the other descriptors
     * used in the compilation process, this is the one the JVM wants, so this one
     * includes any necessary trailing semicolon (e.g. Ljava/lang/String; rather than
     * Ljava/lang/String)
     *
     * @param clazz a class
     * @return the JVM descriptor for the class
     */
    public static String toJvmDescriptor(Class<?> clazz) {
        StringBuilder sb = new StringBuilder();
        if (clazz.isArray()) {
            while (clazz.isArray()) {
                sb.append("[");
                clazz = clazz.getComponentType();
            }
        }
        if (clazz.isPrimitive()) {
            if (clazz == Void.TYPE) {
                sb.append('V');
            } else if (clazz == Integer.TYPE) {
                sb.append('I');
            } else if (clazz == Boolean.TYPE) {
                sb.append('Z');
            } else if (clazz == Character.TYPE) {
                sb.append('C');
            } else if (clazz == Long.TYPE) {
                sb.append('J');
            } else if (clazz == Double.TYPE) {
                sb.append('D');
            } else if (clazz == Float.TYPE) {
                sb.append('F');
            } else if (clazz == Byte.TYPE) {
                sb.append('B');
            } else if (clazz == Short.TYPE) {
                sb.append('S');
            }
        } else {
            sb.append("L");
            sb.append(clazz.getName().replace('.', '/'));
            sb.append(";");
        }
        return sb.toString();
    }

    /**
     * Determine the descriptor for an object instance (or {@code null}).
     *
     * @param value an object (possibly {@code null})
     * @return the type descriptor for the object
     * (descriptor is "Ljava/lang/Object" for {@code null} value)
     */
    public static String toDescriptorFromObject(Object value) {
        if (value == null) {
            return "Ljava/lang/Object";
        } else {
            return toDescriptor(value.getClass());
        }
    }

    /**
     * @param descriptor type descriptor
     * @return {@code true} if the descriptor is for a boolean primitive or boolean reference type
     */
    public static boolean isBooleanCompatible(String descriptor) {
        return (descriptor != null && (descriptor.equals("Z") || descriptor.equals("Ljava/lang/Boolean")));
    }

    /**
     * @param descriptor type descriptor
     * @return {@code true} if the descriptor is for a primitive type
     */
    public static boolean isPrimitive(String descriptor) {
        return (descriptor != null && descriptor.length() == 1);
    }

    /**
     * @param descriptor the descriptor for a possible primitive array
     * @return {@code true} if the descriptor is for a primitive array (e.g. "[[I")
     */
    public static boolean isPrimitiveArray(String descriptor) {
        boolean primitive = true;
        for (int i = 0, max = descriptor.length(); i < max; i++) {
            char ch = descriptor.charAt(i);
            if (ch == '[') {
                continue;
            }
            primitive = (ch != 'L');
            break;
        }
        return primitive;
    }

    /**
     * Determine if boxing/unboxing can get from one type to the other. Assumes at least
     * one of the types is in boxed form (i.e. single char descriptor).
     *
     * @return {@code true} if it is possible to get (via boxing) from one descriptor to the other
     */
    public static boolean areBoxingCompatible(String desc1, String desc2) {
        if (desc1.equals(desc2)) {
            return true;
        }
        if (desc1.length() == 1) {
            if (desc1.equals("Z")) {
                return desc2.equals("Ljava/lang/Boolean");
            } else if (desc1.equals("D")) {
                return desc2.equals("Ljava/lang/Double");
            } else if (desc1.equals("F")) {
                return desc2.equals("Ljava/lang/Float");
            } else if (desc1.equals("I")) {
                return desc2.equals("Ljava/lang/Integer");
            } else if (desc1.equals("J")) {
                return desc2.equals("Ljava/lang/Long");
            }
        } else if (desc2.length() == 1) {
            if (desc2.equals("Z")) {
                return desc1.equals("Ljava/lang/Boolean");
            } else if (desc2.equals("D")) {
                return desc1.equals("Ljava/lang/Double");
            } else if (desc2.equals("F")) {
                return desc1.equals("Ljava/lang/Float");
            } else if (desc2.equals("I")) {
                return desc1.equals("Ljava/lang/Integer");
            } else if (desc2.equals("J")) {
                return desc1.equals("Ljava/lang/Long");
            }
        }
        return false;
    }

    /**
     * Determine if the supplied descriptor is for a supported number type or boolean. The
     * compilation process only (currently) supports certain number types. These are
     * double, float, long and int.
     *
     * @param descriptor the descriptor for a type
     * @return {@code true} if the descriptor is for a supported numeric type or boolean
     */
    public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(String descriptor) {
        if (descriptor == null) {
            return false;
        }
        if (isPrimitiveOrUnboxableSupportedNumber(descriptor)) {
            return true;
        }
        return ("Z".equals(descriptor) || descriptor.equals("Ljava/lang/Boolean"));
    }

    /**
     * Determine if the supplied descriptor is for a supported number. The compilation
     * process only (currently) supports certain number types. These are double, float,
     * long and int.
     *
     * @param descriptor the descriptor for a type
     * @return {@code true} if the descriptor is for a supported numeric type
     */
    public static boolean isPrimitiveOrUnboxableSupportedNumber(String descriptor) {
        if (descriptor == null) {
            return false;
        }
        if (descriptor.length() == 1) {
            return "DFIJ".contains(descriptor);
        }
        if (descriptor.startsWith("Ljava/lang/")) {
            String name = descriptor.substring("Ljava/lang/".length());
            if (name.equals("Double") || name.equals("Float") || name.equals("Integer") || name.equals("Long")) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determine whether the given number is to be considered as an integer
     * for the purposes of a numeric operation at the bytecode level.
     *
     * @param number the number to check
     * @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte}
     */
    public static boolean isIntegerForNumericOp(Number number) {
        return (number instanceof Integer || number instanceof Short || number instanceof Byte);
    }

    /**
     * @param descriptor a descriptor for a type that should have a primitive representation
     * @return the single character descriptor for a primitive input descriptor
     */
    public static char toPrimitiveTargetDesc(String descriptor) {
        if (descriptor.length() == 1) {
            return descriptor.charAt(0);
        } else if (descriptor.equals("Ljava/lang/Boolean")) {
            return 'Z';
        } else if (descriptor.equals("Ljava/lang/Byte")) {
            return 'B';
        } else if (descriptor.equals("Ljava/lang/Character")) {
            return 'C';
        } else if (descriptor.equals("Ljava/lang/Double")) {
            return 'D';
        } else if (descriptor.equals("Ljava/lang/Float")) {
            return 'F';
        } else if (descriptor.equals("Ljava/lang/Integer")) {
            return 'I';
        } else if (descriptor.equals("Ljava/lang/Long")) {
            return 'J';
        } else if (descriptor.equals("Ljava/lang/Short")) {
            return 'S';
        } else {
            throw new IllegalStateException("No primitive for '" + descriptor + "'");
        }
    }

    /**
     * Insert the appropriate CHECKCAST instruction for the supplied descriptor.
     *
     * @param mv         the target visitor into which the instruction should be inserted
     * @param descriptor the descriptor of the type to cast to
     */
    public static void insertCheckCast(MethodVisitor mv, String descriptor) {
        if (descriptor.length() != 1) {
            if (descriptor.charAt(0) == '[') {
                if (isPrimitiveArray(descriptor)) {
                    mv.visitTypeInsn(CHECKCAST, descriptor);
                } else {
                    mv.visitTypeInsn(CHECKCAST, descriptor + ";");
                }
            } else {
                if (!descriptor.equals("Ljava/lang/Object")) {
                    // This is chopping off the 'L' to leave us with "java/lang/String"
                    mv.visitTypeInsn(CHECKCAST, descriptor.substring(1));
                }
            }
        }
    }

    /**
     * Determine the appropriate boxing instruction for a specific type (if it needs
     * boxing) and insert the instruction into the supplied visitor.
     *
     * @param mv         the target visitor for the new instructions
     * @param descriptor the descriptor of a type that may or may not need boxing
     */
    public static void insertBoxIfNecessary(MethodVisitor mv, String descriptor) {
        if (descriptor.length() == 1) {
            insertBoxIfNecessary(mv, descriptor.charAt(0));
        }
    }

    /**
     * Determine the appropriate boxing instruction for a specific type (if it needs
     * boxing) and insert the instruction into the supplied visitor.
     *
     * @param mv the target visitor for the new instructions
     * @param ch the descriptor of the type that might need boxing
     */
    public static void insertBoxIfNecessary(MethodVisitor mv, char ch) {
        switch (ch) {
            case 'Z':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
                break;
            case 'B':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
                break;
            case 'C':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
                break;
            case 'D':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
                break;
            case 'F':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
                break;
            case 'I':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
                break;
            case 'J':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
                break;
            case 'S':
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
                break;
            case 'L':
            case 'V':
            case '[':
                // no box needed
                break;
            default:
                throw new IllegalArgumentException("Boxing should not be attempted for descriptor '" + ch + "'");
        }
    }

    /**
     * Deduce the descriptor for a type. Descriptors are like JVM type names but missing
     * the trailing ';' so for Object the descriptor is "Ljava/lang/Object" for int it is
     * "I".
     *
     * @param type the type (may be primitive) for which to determine the descriptor
     * @return the descriptor
     */
    public static String toDescriptor(Class<?> type) {
        String name = type.getName();
        if (type.isPrimitive()) {
            switch (name.length()) {
                case 3:
                    return "I";
                case 4:
                    if (name.equals("byte")) {
                        return "B";
                    } else if (name.equals("char")) {
                        return "C";
                    } else if (name.equals("long")) {
                        return "J";
                    } else if (name.equals("void")) {
                        return "V";
                    }
                    break;
                case 5:
                    if (name.equals("float")) {
                        return "F";
                    } else if (name.equals("short")) {
                        return "S";
                    }
                    break;
                case 6:
                    if (name.equals("double")) {
                        return "D";
                    }
                    break;
                case 7:
                    if (name.equals("boolean")) {
                        return "Z";
                    }
                    break;
            }
        } else {
            if (name.charAt(0) != '[') {
                return "L" + type.getName().replace('.', '/');
            } else {
                if (name.endsWith(";")) {
                    return name.substring(0, name.length() - 1).replace('.', '/');
                } else {
                    return name;  // array has primitive component type
                }
            }
        }
        return null;
    }

    /**
     * Create an array of descriptors representing the parameter types for the supplied
     * method. Returns a zero sized array if there are no parameters.
     *
     * @param method a Method
     * @return a String array of descriptors, one entry for each method parameter
     */
    public static String[] toParamDescriptors(Method method) {
        return toDescriptors(method.getParameterTypes());
    }

    /**
     * Create an array of descriptors representing the parameter types for the supplied
     * constructor. Returns a zero sized array if there are no parameters.
     *
     * @param ctor a Constructor
     * @return a String array of descriptors, one entry for each constructor parameter
     */
    public static String[] toParamDescriptors(Constructor<?> ctor) {
        return toDescriptors(ctor.getParameterTypes());
    }

    /**
     * Create an array of descriptors from an array of classes.
     *
     * @param types the input array of classes
     * @return an array of descriptors
     */
    public static String[] toDescriptors(Class<?>[] types) {
        int typesCount = types.length;
        String[] descriptors = new String[typesCount];
        for (int p = 0; p < typesCount; p++) {
            descriptors[p] = toDescriptor(types[p]);
        }
        return descriptors;
    }

    /**
     * Create the optimal instruction for loading a number on the stack.
     *
     * @param mv    where to insert the bytecode
     * @param value the value to be loaded
     */
    public static void insertOptimalLoad(MethodVisitor mv, int value) {
        if (value < 6) {
            mv.visitInsn(ICONST_0 + value);
        } else if (value < Byte.MAX_VALUE) {
            mv.visitIntInsn(BIPUSH, value);
        } else if (value < Short.MAX_VALUE) {
            mv.visitIntInsn(SIPUSH, value);
        } else {
            mv.visitLdcInsn(value);
        }
    }

    /**
     * Produce appropriate bytecode to store a stack item in an array. The
     * instruction to use varies depending on whether the type
     * is a primitive or reference type.
     *
     * @param mv               where to insert the bytecode
     * @param arrayElementType the type of the array elements
     */
    public static void insertArrayStore(MethodVisitor mv, String arrayElementType) {
        if (arrayElementType.length() == 1) {
            switch (arrayElementType.charAt(0)) {
                case 'I':
                    mv.visitInsn(IASTORE);
                    break;
                case 'J':
                    mv.visitInsn(LASTORE);
                    break;
                case 'F':
                    mv.visitInsn(FASTORE);
                    break;
                case 'D':
                    mv.visitInsn(DASTORE);
                    break;
                case 'B':
                    mv.visitInsn(BASTORE);
                    break;
                case 'C':
                    mv.visitInsn(CASTORE);
                    break;
                case 'S':
                    mv.visitInsn(SASTORE);
                    break;
                case 'Z':
                    mv.visitInsn(BASTORE);
                    break;
                default:
                    throw new IllegalArgumentException("Unexpected arraytype " + arrayElementType.charAt(0));
            }
        } else {
            mv.visitInsn(AASTORE);
        }
    }

    /**
     * Determine the appropriate T tag to use for the NEWARRAY bytecode.
     *
     * @param arraytype the array primitive component type
     * @return the T tag to use for NEWARRAY
     */
    public static int arrayCodeFor(String arraytype) {
        switch (arraytype.charAt(0)) {
            case 'I':
                return T_INT;
            case 'J':
                return T_LONG;
            case 'F':
                return T_FLOAT;
            case 'D':
                return T_DOUBLE;
            case 'B':
                return T_BYTE;
            case 'C':
                return T_CHAR;
            case 'S':
                return T_SHORT;
            case 'Z':
                return T_BOOLEAN;
            default:
                throw new IllegalArgumentException("Unexpected arraytype " + arraytype.charAt(0));
        }
    }

    /**
     * @return true if the supplied array type has a core component reference type
     */
    public static boolean isReferenceTypeArray(String arraytype) {
        int length = arraytype.length();
        for (int i = 0; i < length; i++) {
            char ch = arraytype.charAt(i);
            if (ch == '[') continue;
            return ch == 'L';
        }
        return false;
    }

    /**
     * Produce the correct bytecode to build an array. The opcode to use and the
     * signature to pass along with the opcode can vary depending on the signature
     * of the array type.
     *
     * @param mv        the methodvisitor into which code should be inserted
     * @param size      the size of the array
     * @param arraytype the type of the array
     */
    public static void insertNewArrayCode(MethodVisitor mv, int size, String arraytype) {
        insertOptimalLoad(mv, size);
        if (arraytype.length() == 1) {
            mv.visitIntInsn(NEWARRAY, CodeFlow.arrayCodeFor(arraytype));
        } else {
            if (arraytype.charAt(0) == '[') {
                // Handling the nested array case here. If vararg
                // is [[I then we want [I and not [I;
                if (CodeFlow.isReferenceTypeArray(arraytype)) {
                    mv.visitTypeInsn(ANEWARRAY, arraytype + ";");
                } else {
                    mv.visitTypeInsn(ANEWARRAY, arraytype);
                }
            } else {
                mv.visitTypeInsn(ANEWARRAY, arraytype.substring(1));
            }
        }
    }

    /**
     * For use in mathematical operators, handles converting from a (possibly boxed)
     * number on the stack to a primitive numeric type.
     * <p>For example, from a Integer to a double, just need to call 'Number.doubleValue()'
     * but from an int to a double, need to use the bytecode 'i2d'.
     *
     * @param mv               the method visitor when instructions should be appended
     * @param stackDescriptor  a descriptor of the operand on the stack
     * @param targetDescriptor a primitive type descriptor
     */
    public static void insertNumericUnboxOrPrimitiveTypeCoercion(
            MethodVisitor mv, String stackDescriptor, char targetDescriptor) {

        if (!CodeFlow.isPrimitive(stackDescriptor)) {
            CodeFlow.insertUnboxNumberInsns(mv, targetDescriptor, stackDescriptor);
        } else {
            CodeFlow.insertAnyNecessaryTypeConversionBytecodes(mv, targetDescriptor, stackDescriptor);
        }
    }


    interface FieldAdder {

        void generateField(ClassWriter cw, CodeFlow codeflow);
    }


    interface ClinitAdder {

        void generateCode(MethodVisitor mv, CodeFlow codeflow);
    }

}
